package org.mtmi.ui.controls;

import java.util.ArrayList;

import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Point;
import org.mtmi.io.IControlIO;
import org.mtmi.io.controls.KeyboardIO;
import org.mtmi.tuio.listeners.DragListener;
import org.mtmi.tuio.listeners.SingleTapListener;
import org.mtmi.tuio.listeners.TUIODragEvent;
import org.mtmi.tuio.listeners.TUIOTapEvent;
import org.mtmi.ui.GraphicUtil;
import org.mtmi.ui.IAdaptable;
import org.mtmi.ui.controls.edits.KeyboardControlEditor;
import org.mtmi.ui.controls.listeners.ControlDisposedListener;
import org.mtmi.ui.controls.listeners.ValueChangedListener;
import org.mtmi.ui.controls.listeners.DisposeEvent;
import org.mtmi.ui.controls.listeners.ValueEvent;
import org.mtmi.ui.controls.listeners.KeyboardEvent;
import org.mtmi.ui.controls.listeners.KeyboardKeyStateEvent;
import org.mtmi.ui.controls.listeners.KeyboardListener;
import org.mtmi.ui.controls.listeners.KeyboardOctaveEvent;
import org.mtmi.ui.edit.IControlEditor;
import org.mtmi.ui.layouts.LayoutData;
import org.mtmi.ui.scenes.COLOR;
import org.mtmi.ui.scenes.Scene;

public class Keyboard extends Control 
  implements ITouchableControl, SingleTapListener, DragListener, 
             ControlDisposedListener, ValueChangedListener
{
	
	private int value0; //value for leftward key 
	private int transpose; //current transpose value;
	private int octave_count; //number of octave displayed
	private boolean down[];
	private ArrayList<KeyboardListener> lis_keyboard;

	public Keyboard( Scene scene,  int _flags ) {
		super( scene, _flags );
		octave_count = 1;
		size.x = getPreferredWidthForHeight(150);
		size.y = 150;
		down = new boolean [12];
		value0 = 60; //C3 by default
		transpose = 0;
		updateMetrics();
	}
	
	@Override
	public Point computeSize(LayoutData l) {
		return new Point( getPreferredWidthForHeight(150), 150 );
	}
	
	public int getOctaveCount() { return octave_count; }
	
	public void setOctaveCount( int cnt ) { 
		int noc = Math.max( 1, cnt);
		if( noc!=octave_count ) {
			int oldc = octave_count;
			octave_count = noc;
			down = new boolean[12*octave_count];
			updateMetrics();
			KeyboardOctaveEvent event = new KeyboardOctaveEvent( this, oldc );
			notifyKeyboardListeners( event );
		}
	}
	
	/** change value of leftward key, this don't reset any transposition */
	public void setValue0( int v0 ) {
		value0 = v0;
	}
	
	/** @return the value of leftward key, does not take care of any transposition */
	public int getValue0() {
		return value0;
	}
	
	/** Change current transposition by incrementing current transpose value */
	public void transpose( int dv ) {
		transpose += dv;
	}
	
	/** @return current value of transposition of keys */
	public int getTranspose() { return transpose; }
	
	/** Change the current transpose value */
	public void setTranspose( int abs ) {
		transpose = abs;
	}
	
	public int getPreferredWidthForHeight( int h ) {
		//ca c'est un ratio sympa ... pour le ratio de mon ecran
		// faudrait prendre en compte le ratio de mon ecran pour etre plus universel
		return 400*h*octave_count/175;
	}
	
	@Override
	public void setBounds( int x, int y, int w, int h ) {
		super.setBounds( x, y, w, h );
		updateMetrics();
	}
	
	//metrique issu de la taille courante
	private int hb, wb; // height, width black key
	private int ww; //width white key
	private int xb0; // left location of first black key.
	private int db; // distance to draw '3D' on black keys
	private void updateMetrics() {
		hb = 2*size.y/3;
		ww = size.x / (7*octave_count);
		wb = 2*ww/3; 
		xb0 = ww-wb/2;
		db = Math.round( wb/10f );
		if( db < 2 && wb >6 ) {
			db = 2;
		}
	}
	
	/**
	 * @param i index of key in octave 
	 * >=0: white key, return it index (0..6)
	 * <0:  black key, return it index (-1..-6), -3 must never be returned */
	private int getKey( int i ) {
		switch( i ) {
		case 0: return 0;
		case 1: return -1;
		case 2: return 1;
		case 3: return -2;
		case 4: return 2;
		case 5: return 3;
		case 6: return -4;
		case 7: return 4;
		case 8: return -5;
		case 9: return 5;
		case 10: return -6;
		case 11:return 6;
		default:
			return 0; //by default...
		}
	}
	
	@Override
	public void paint(GC gc) {
		
//		gc.setAntialias(SWT.ON);
//		gc.setTextAntialias(SWT.ON);
//		gc.setFont(FontManager.getInstance().getNormalFont());
		gc.setLineWidth( 0 );
		
		//fond blanc
		Color white = COLOR.grey90;
		gc.setBackground( white );
		gc.fillRectangle( location.x, location.y, 7*octave_count*ww, size.y );

		int btm = location.y+size.y;
		int btmb = location.y+hb;
		
		int xx0=location.x;
		int k0=0;
		for( int o=0; o<octave_count; o++, k0+=12 ) {
			
			
			//touches enfoncées
			gc.setBackground( COLOR.grey70 );
			gc.setForeground( COLOR.grey100 );
			
			for( int i=0; i<12; i++ ) {
				int ik = getKey( i );
				if( down[i+k0] ) {
					if( ik >=0 ) { //white key:
						gc.fillRectangle( xx0+ik*ww, location.y, ww, size.y );
					} else { //black
						int xb = (-ik-1)*ww+xb0+xx0;
						gc.fillRectangle( xb, location.y, wb, hb );
						/*gc.drawLine( xb+4, 0, xb+4, hb-4 );
					gc.drawLine( xb+4, hb-4, xb+wb, hb-4 );*/
						gc.drawLine( xb+db, location.y, xb, btmb );
					}
				}
			}
		
			//touches noire
			gc.setBackground( COLOR.grey0 );
			gc.setForeground( COLOR.grey80 );
			int xb = xb0+xx0;
			int k=1+k0;
			for( int i=0; i<5; i++ ) {
				if( !down[k] ) { //down key are already drawn
					gc.fillRectangle( xb, location.y, wb, hb );
					gc.drawLine( xb+db, location.y, xb+db, btmb-db );
					gc.drawLine( xb+db, btmb-db, xb+wb-db, btmb-db );
				}
				xb += ww;
				k+=2;
				if( i==1 ) {
					xb += ww;
					k++;
				}
			}

			//separation touches blanches
			int xw = ww+xx0;
			for( int i=0; i<7; i++ ) {
				int yw = (i==2||i==6 ? 0 : hb) + location.y;
				gc.setForeground( COLOR.grey80 );
				gc.drawLine( xw, yw, xw, btm-1 );
				gc.setForeground( COLOR.grey100 );
				gc.drawLine( xw+1, yw, xw+1, btm-1 );
				xw += ww;
			}
			gc.drawLine( location.x, location.y, location.x, btm );
			
			//octave suivante
			xx0+= 7*ww;
		}
	}

	@Override
	public boolean containsTouch(TouchControlEvent e) {
		return GraphicUtil.rectContains( location, size, e.x, e.y );
	}

	/*** @depracated TODO remove
	@Override
	public Region getRegion() {
		Region r = new Region();
		r.add( location.x, location.y, size.x, size.y );
		return r;
	} */

	@Override
	public String toString() {
		return super.toString()+"{value0:"+value0+"}";
	}
	
	/** @return key index (0..11)*octave for relative location, -1 for outside */
	private int getKeyAt( int x, int y ) {
		int fw = octave_count*7*ww; // may differ from size.x, due to (int) truncation
		if( x>=0 && y>=0 && x< fw && y <= size.y ) {
			
			int octave = x / (7*ww);
			
			int dx = x % (7*ww);
			
			//check black key first:
			if( y < hb ) {
				int xb = xb0;
				int ik=1;
				for( int i=0; i<5; i++ ) {
					if( dx>=xb && dx<=xb+wb && y <= hb ) {
						return ik+12*octave; 
					}
					ik+=2;
					xb +=ww;
					if( i==1 ) {
						ik++; //6 indeed
						xb += ww;
					}
				}
			}
			//can check white keys:
			int ik=0;
			int xw=0;
			for( int i=0; i<7; i++ ) {
				if( dx<= xw+ww ) {
					return ik+12*octave;
				}
				ik += 2;
				if( i==2 ) ik--; //ik=5 indeed
				xw += ww;
			}
		}
		//outside
		return -1;
	}

	@Override
	public void tapDown(TUIOTapEvent event) {
		int k = getKeyAt( event.ex-location.x, event.ey-location.y );
		if( k>=0 ) {
			down[k] = true;
			redraw();
			KeyboardEvent e = new KeyboardKeyStateEvent( this, 1, new int[]{value0+transpose+k}, new boolean[]{true} );
			notifyKeyboardListeners( e );
		} else {
			//on devrait pas etre en dehors....
		}
	}

	@Override
	public void tapUp(TUIOTapEvent event) {
		int k = getKeyAt( event.ex-location.x, event.ey-location.y );
		if( k>=0 ) {
			down[k] = false;
			redraw();
			KeyboardEvent e = new KeyboardKeyStateEvent( this, 1, new int[]{value0+transpose+k}, new boolean[]{false} );
			notifyKeyboardListeners( e );
		}
	}
	
	@Override
	public void drag( TUIODragEvent event ) {
		int k = getKeyAt( event.ex-location.x, event.ey-location.y );
		int ndown = 0;
		for( int i=0; i<down.length; i++ ) {
			if( down[i] ) ndown++;
		}
		if( k>=0 ) {
			if( !down[k] ) {
				int key[] = new int[ ndown+1 ];
				boolean st[] = new boolean [ key.length ];
				int iw=0;
				for( int i=0; i<down.length; i++ ) {
					if( down[i] ) {
						key[iw] = value0+transpose+i;
						st[iw] = false;
						iw++;
					}
				}
				//pour le moment on force un "single" tap, car on sait pas celui qui drag et les autre y sont o� ?
				for( int i=0; i<down.length; i++ ) down[i]=false;
				down[k] = true;
				redraw();

				key[iw] = value0+transpose+k;
				st[iw] = true;
				KeyboardEvent e = new KeyboardKeyStateEvent( this, key.length, key, st );
				notifyKeyboardListeners( e );
			}
		}
		else if( ndown > 0 ){
			int key[] = new int[ ndown ];
			boolean st[] = new boolean [ ndown ];
			int iw=0;
			for( int i=0; i<down.length; i++ ) {
				if( down[i] ) {
					key[iw] = value0+transpose+i;
					st[iw] = false;
					iw++;
					down[i] = false; //set key up
				}
			}
			redraw();
			KeyboardEvent e = new KeyboardKeyStateEvent( this, key.length, key, st );
			notifyKeyboardListeners( e );
		}
		
	}
	
	protected void notifyKeyboardListeners( KeyboardEvent event ) {
		if( lis_keyboard == null ) return ;
		
		KeyboardListener lis[] = lis_keyboard.toArray( new KeyboardListener[lis_keyboard.size()] );
		
		if( event instanceof KeyboardKeyStateEvent ) {
			KeyboardKeyStateEvent ke = (KeyboardKeyStateEvent)event;
			for ( int i=0; i<lis.length; i++ ) {
				lis[i].keyboardKeyStateChanged( ke );
			}
		} else {
			KeyboardOctaveEvent oe = (KeyboardOctaveEvent)event;
			for ( int i=0; i<lis.length; i++ ) {
				lis[i].keyboardOctaveCountChanged( oe );
			}
		}
	}
	
	public void addKeyboardListener( KeyboardListener listener ) {
		if( lis_keyboard==null ) {
			lis_keyboard = new ArrayList<KeyboardListener>();
		}
		lis_keyboard.add( listener );
	}
	
	public void removeKeyboardListener(KeyboardListener listener) {
		if( lis_keyboard!=null ) {
			lis_keyboard.remove( listener );
		}
	}

	@Override
	public Object getAdapter( Class<?> clazz, Object arg ) {
		if( clazz == IControlEditor.class ) {
			return new KeyboardControlEditor( this );
		}
		if( clazz == IControlIO.class ) {
			return new KeyboardIO();
		}
		return super.getAdapter( clazz, arg );
	}
	

	// a control modifying dynamically transpose value
	private IValueGenerator ctr_transpose;
	private String          ctr_transpose_connection_id;
	
	public IValueGenerator getTransposeController() { return ctr_transpose; }
	public String getTransposeControllerConnectionId() { return ctr_transpose_connection_id; }
	
	private void updateTransposeFromTransposeControl() {
		if( ctr_transpose!= null ) {
			float k = ctr_transpose.getValueF();
			//convert range to -5..+5;
			int v = Math.round(10*k-5);
			v *= 12; //octave transposition
			setTranspose( v );
		}
	}
	
	public void setTransposeController( IValueGenerator tc, String cid ) {
		if( ctr_transpose!= null ) {
			if( ctr_transpose instanceof IAdaptable ) {
				Control c = (Control)((IAdaptable)ctr_transpose).getAdapter( Control.class, null );
				c.removeControlDisposedListener( this );
			}
			ctr_transpose.removeValueChangedListener( this );
		}
		ctr_transpose = tc;
		ctr_transpose_connection_id = cid;
		if( ctr_transpose!=null ) {
			ctr_transpose.addValueChangedListener( this );
			if( ctr_transpose instanceof IAdaptable ) {
				Control c = (Control)((IAdaptable)ctr_transpose).getAdapter( Control.class, null );
				c.addControlDisposedListener( this );
			}
			updateTransposeFromTransposeControl();
		}
	}
	
	@Override
	public void controlDisposed( DisposeEvent event ) {
		if( event.control == ctr_transpose ) {
			ctr_transpose.removeValueChangedListener( this );
			ctr_transpose = null;
		}
	}
	
	@Override
	public void valueChanged( ValueEvent event ) {
		if( event.control == ctr_transpose ) {
			updateTransposeFromTransposeControl();
		}
	}
	
}
